Scripted objects migration/pl

Wprowadzenie

Obiekty tworzone skryptami są odbudowywane przy każdym otwarciu dokumentu FCStd. W tym celu dokument przechowuje referencję do modułu i klasy Pythona, które zostały użyte do stworzenia obiektu, wraz z jego właściwościami.

<Document SchemaVersion="4" ProgramVersion="0.19R20959 (Git)" FileVersion="1">
    ...
    <Properties Count="15" TransientCount="3">
    ...
    </Properties>
    <Objects Count="1" Dependencies="1">
        <ObjectDeps Name="Custom" Count="0"/>
        <Object type="Part::FeaturePython" name="Custom" id="2715" Touched="1" />
    </Objects>
    <ObjectData Count="1">
        <Object name="Custom">
            <Properties Count="9" TransientCount="0">
                ...
                <Property name="Proxy" type="App::PropertyPythonObject" status="1">
                    <Python value="eyJUeXBlIjogIkN1c3RvbSJ9" encoded="yes" module="old_module" class="OldObject"/>
                </Property>
                ...
            </Properties>
        </Object>
    </ObjectData>
</Document>

Szczególnie skup się na tej części:

...
                <Property name="Proxy" type="App::PropertyPythonObject" status="1">
                    <Python value="eyJUeXBlIjogIkN1c3RvbSJ9" encoded="yes" module="old_module" class="OldObject"/>
                </Property>
                ...

Jeśli wartość module= lub class= nie zostanie znaleziona w zainstalowanym systemie, obiekt nie zostanie poprawnie załadowany. Oznacza to, że po utworzeniu obiektu przy użyciu określonej klasy, moduł nie powinien być już przenoszony ani zmieniany, ponieważ jeśli to nastąpi, wcześniej zapisane obiekty ulegną uszkodzeniu.

Jednak ważnym powodem przeniesienia lub zmiany nazwy modułu lub klasy jest poprawa struktury i łatwości konserwacji oryginalnego kodu, na przykład podczas restrukturyzacji całego środowiska pracy. W takim przypadku istnieją różne strategie migracji starych obiektów do nowej klasy. Odbywa się to w celu zachowania kompatybilności wstecznej, gdy należy unikać jawnego zrywania funkcjonalności starych dokumentów.

Stary i nowy obiekt

Stary obiekt

Stary obiekt jest zdefiniowany w module, który znajduje się w katalogu głównym środowiska pracy.

# old_module.py
class OldObject:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "Area")
        obj.Length = 15
        obj.Area = 300
        obj.Proxy = self
        self.Type = "Custom"

    def execute(self, obj):
        pass

Obiekt może zostać utworzony przy użyciu tej klasy i zapisany do pliku my_document.FCstd. Jeśli żaden konkretny dostawca widoku nie jest przypisany do nowego obiektu, jego klasa proxy jest po prostu ustawiana na wartość inną niż None, w tym przypadku na 1.

import FreeCAD as App
import old_module

doc = App.newDocument()
doc.FileName = "my_document.FCStd"

obj = doc.addObject("Part::FeaturePython", "Custom")
old_module.OldObject(obj)

if App.GuiUp:
    obj.ViewObject.Proxy = 1

doc.recompute()
doc.save()

Sesja konsoli Python z pominiętymi podstawowymi właściwościami.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', ..., ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<old_module.OldObject object at 0x7efc3c51c390>

Nowy obiekt

Weźmy teraz pod uwagę, że środowisko pracy jest zrestrukturyzowane tak, że klasy nie znajdują się tylko w katalogu głównym, ale zamiast tego znajdują się wewnątrz katalogu objects. Złożone środowiska pracy, które mają wiele różnych typów obiektów, powinny być zorganizowane w katalogach zawierających obiekty, dostawcy widoku, polecenia Gui, interfejsy panela zadań itd.

# objects/new_module.py
class NewObject:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "GeneralArea")
        obj.addProperty("App::PropertyInteger", "Divisions")
        obj.Length = 30
        obj.GeneralArea = 600
        obj.Divisions = 4
        obj.Proxy = self
        self.Type = "Custom"

    def execute(self, obj):
        pass

Ta nowa klasa będzie odnosić się do tego samego typu obiektu, ale zarówno nazwa modułu, jak i nazwa klasy zostały zmienione. Co więcej, właściwości również uległy zmianie. Zmieniono nazwę jednej właściwości i dodano zupełnie nową właściwość.

Jeśli utworzymy nowy obiekt z tym nowym modułem, otrzymamy następującą sesję konsoli.

>>> obj2 = App.ActiveDocument.Custom2
>>> print(obj2.PropertiesList)
['Divisions', ..., 'GeneralArea', ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj2.Proxy)
<objects.new_module.NewObject object at 0x7efc1cf68c50>

Metoda 1. Migracja poprzez przekierowanie klasy

Zmigrujemy starszy obiekt poprzez przekierowanie starej klasy. Oryginalna klasa jest usuwana, a nazwa klasy jest po prostu przekierowywana, aby wskazywała na nową klasę.

# old_module.py
import objects.new_module as new_module

OldObject = new_module.NewObject

Każdy dokument, który spróbuje załadować old_module.OldObject zostanie przekierowany do załadowania objects.new_module.NewObject zamiast niego.

Jeśli otworzymy dokument i sprawdzimy właściwości obiektu w konsoli Python, zobaczymy, że starsze właściwości zostały zachowane, ale obiekt ma nową klasę Proxy.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', ..., ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7f099700b2b0>

Jednak w tym przypadku nie widzimy nowych właściwości nowej klasy. Powodem jest po prostu to, że starszy obiekt nie miał tych właściwości. Kiedy old_module.OldObject został przekierowany do objects.new_module.NewObject, zmieniła się tylko klasa proxy, ale poprzednie informacje zostały zachowane.

Teraz, jeśli dokument zostanie zapisany i otwarty ponownie, będzie automatycznie szukał objects.new_module.NewObject i nie będzie już wymagał old_module.OldObject. Plik old_module.py może zostać trwale usunięty z systemu, o ile wszystkie starsze obiekty zostały zmigrowane do nowego modułu. Jeśli stary moduł zostanie usunięty, ale obiekt nie został zmigrowany, widok raportu wyświetli taki komunikat podczas otwierania dokumentu zawierającego taki obiekt.

<class 'ModuleNotFoundError'>: No module named 'old_module'

Jeśli migracja wszystkich starszych obiektów nie jest realistycznie możliwa, na przykład dlatego, że stary moduł był używany w środowisku pracy przez wiele lat, old_module.py musi zostać zachowany tak długo, jak jest to konieczne, aby dać użytkownikom możliwość migracji ich obiektów.

Zalety i wady

Zalety

Wady

Metoda 2. Migracja podczas przywracania dokumentu

Zmigrujemy starszy obiekt, modyfikując starą klasę. Większość oryginalnej klasy zostanie usunięta, a zamiast tego zaimplementowana zostanie metoda onDocumentRestored. Gdy ta metoda istnieje, zostanie uruchomiona, gdy dokument spróbuje przywrócić obiekt korzystający z tej klasy, więc jest to okazja, aby przypisać nową klasę, manipulować informacjami lub drukować komunikaty.

W tym przypadku zakładamy, że zdefiniowaliśmy również nowego dostawcę widoku w module viewp/new_view.py. Jeśli nie chcemy migrować tej klasy, możemy pominąć wszystko po sprawdzeniu App.GuiUp.

# old_module.py
import FreeCAD as App
import objects.new_module as new_module
import viewp.new_view as new_view
_wrn = App.Console.PrintWarning

class OldObject:
    def onDocumentRestored(self, obj):
        new_module.NewObject(obj)
        _wrn("New proxy class used\n")

        if App.GuiUp:
            new_view.ViewProviderNew(obj.ViewObject)
            _wrn("New viewprovider class used\n")

Bardziej złożony przykład sprawdza najpierw, czy klasa proxy jest typu, którego szukamy, i kontynuuje migrację tylko wtedy, gdy jest to właściwy typ.

class OldObject:
    def onDocumentRestored(self, obj):
        if hasattr(obj, "Proxy") and obj.Proxy.Type == "Custom":
            _module = str(obj.Proxy.__class__)
            _module = _module.lstrip("<class '").rstrip("'>")

            if _module == "old_module.OldObject":
                self._migrate(obj)

    def _migrate(self, obj):
        _wrn("New proxy class used\n")
        new_module.NewObject(obj)

        if App.GuiUp:
            new_view.ViewProviderNew(obj.ViewObject)
            _wrn("New viewprovider class used\n")

Zakładając, że zmieniliśmy już w ten sposób stary moduł, jeśli otworzymy dokument ze starym obiektem, zobaczymy komunikaty wspominające o użyciu nowych klas.

Sprawdzając obiekt z konsoli Python zobaczymy, że starsze właściwości zostały zachowane, a dodatkowo nowe właściwości zostały dodane wraz z nową klasą Proxy.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', 'Divisions', ..., 'GeneralArea', ..., ..., 'Length', 'Length1', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7fecb0ebd7b8>

Stare właściwości to Area i Length; nowe właściwości to Divisions, GeneralArea i Length. Migrowany obiekt zachowuje oryginalne dwie właściwości i zyskuje trzy nowe właściwości. Ponieważ jednak nowa właściwość Length ma taką samą nazwę jak starsza właściwość, nazwa nowej właściwości jest zmieniana na numer przyrostowy. Przypuszczalnie nie tego chcemy. Możemy poprawić sytuację, postępując zgodnie z uzupełnieniem 2.1 poniżej.

Biorąc pod uwagę, że klasy mają obsługiwać ten sam typ obiektu, chcielibyśmy migracji, w której Area przekształca się w GeneralArea, a Length jest po prostu przypisywany do nowego Length i nie ma duplikatów właściwości.

=Zalety i wady

Zalety

Wady

Metoda 3. Migracja podczas przywracania dokumentu, ręczna obsługa właściwości

Jest to rozszerzenie metody 2. W metodzie onDocumentRestored musimy zapisać wartości właściwości, które chcemy, a następnie możemy usunąć te oryginalne właściwości. Odbywa się to tak, że gdy nowa klasa jest używana, przypisuje nowe właściwości bez ryzyka kolizji nazw ze starszymi właściwościami.

Podobnie jak w metodzie 2, jeśli chcemy, możemy również dodać fragment kodu, który sprawdza, czy klasa Proxy jest właściwa. W tym przykładzie ponownie zakładamy, że używamy niestandardowego dostawcy widoku, z co najmniej jedną niestandardową właściwością.

# old_module.py
import FreeCAD as App
import objects.new_module as new_module
import viewp.new_view as new_view
_wrn = App.Console.PrintWarning

class OldObject:
    def onDocumentRestored(self, obj):
        old = dict()
        old["Area"] = obj.Area
        old["Length"] = obj.Length
        obj.removeProperty("Area")
        obj.removeProperty("Length")

        new_module.NewObject(obj)

        obj.GeneralArea = 3 * old["Area"]
        obj.Length = old["Length"]
        _wrn("New proxy class used; properties migrated\n")

        if App.GuiUp:
            vobj = obj.ViewObject
            old = dict()

            old["LineScale"] = vobj.LineScale
            vobj.removeProperty("LineScale")

            new_view.ViewProviderNew(vobj)

            vobj.LineScale = 1.05 * old["LineScale"]
            _wrn("New viewprovider class used; view properties migrated\n")

Widzimy, że stare wartości są przechowywane w słowniku pomocniczym, następnie stare właściwości są usuwane, następnie dodajemy nową klasę, a na koniec przypisujemy wcześniej zapisane wartości do nowych właściwości. W tym momencie możemy przekształcić zapisane wartości zgodnie z potrzebami nowej klasy. Na przykład, GeneralArea jest ustawiony na 3-krotność starego Area, a nowy Length po prostu otrzymuje wartość starego Length. Ponieważ wiemy, jak powinny zachowywać się stare i nowe klasy, możemy swobodnie manipulować danymi, aby zmigrować obiekt tak, jak chcemy.

Możemy usunąć tylko te właściwości, które zostały dodane przez klasy Python podczas tworzenia obiektów generowanych skryptami. Inne atrybuty należą do bazowego obiektu C++ i nie mogą być usunięte.

>>> obj.removeProperty("Visibility")
False

Zakładając, że zmieniliśmy już stary moduł w ten sposób, jeśli otworzymy dokument ze starym obiektem, zobaczymy komunikaty wspominające o użyciu nowych klas. Sprawdzając obiekt z konsoli Python widzimy, że starsze właściwości zostały usunięte i istnieją tylko nowe.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Divisions', ..., 'GeneralArea', ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7efd456c9b00>

Ponieważ w starej klasie właściwość Divisions nie istniała, nic nie zostało z nią zrobione. Została ona po prostu utworzona przez nową klasę objects.new_module.NewObject.

Zalety i wady

Zalety

Wady

Addendum A. Creating the properties only if they do not already exist

Jedną z wad metody 2 jest to, że zawsze będzie ona próbowała dodać nowe właściwości. Jeśli starsze właściwości mają taką samą nazwę jak nowe, zostaną zduplikowane z przyrostową liczbą, więc Length spowoduje Length1, a następnie Length2 i tak dalej. To sprawia, że metoda 2 jest nierealistyczną opcją w większości przypadków, ponieważ nowa klasa i tak będzie używać tylko jednej właściwości.

Aby ulepszyć tę metodę, nową klasę można również zmodyfikować tak, aby dodawała właściwości tylko wtedy, gdy nie istnieją jeszcze pod tą samą nazwą.

# objects/new_module.py
class NewObject:
    def __init__(self, obj):
        if not hasattr(obj, "Length"):
            obj.addProperty("App::PropertyLength", "Length")
            obj.Length = 30
        if not hasattr(obj, "GeneralArea"):
            obj.addProperty("App::PropertyArea", "GeneralArea")
            obj.GeneralArea = 600
        if not hasattr(obj, "Divisions"):
            obj.addProperty("App::PropertyInteger", "Divisions")
            obj.Divisions = 4

        obj.Proxy = self
        self.Type = "Custom"

    def execute(self, obj):
        pass

W tym przypadku, ponieważ Length już istnieje, nie zostanie ponownie dodany; GeneralArea i Divisions nie istnieją, więc zostaną dodane. I tak jak poprzednio, Area zostanie zachowany, ponieważ nie został wyraźnie usunięty, chociaż prawdopodobnie nie jest już używany w nowej klasie.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', 'Divisions', ..., 'GeneralArea', ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7f036bd4c6a0>

To samo można zrobić dla klasy dostawcy widoku.

Używając tej metody 2 + A, wynik jest podobny do metody 1, ponieważ obiekt zachowa wszystkie poprzednie właściwości, ale dodatkowo zyska nowe właściwości dostarczone przez nową klasę.

Metoda 3 nie potrzebuje tego dodatku do nowej klasy, ponieważ starsze właściwości są wyraźnie usuwane, więc nie będzie żadnych konfliktów podczas instalowania nowych właściwości. Niemniej jednak, nadal dobrą praktyką jest, aby każda klasa dodawała swoje wymagane właściwości tylko wtedy, gdy jeszcze nie istnieją. Jest to pomocne zarówno w przypadku tworzenia nowych obiektów generowanych skryptami, jak i ich migracji.

Zalety i wady

Zalety

Wady

Dodatek B. Migracja różnych wersji starego obiektu

Metoda 3 jest najbardziej złożoną metodą, ponieważ właściwości są obsługiwane indywidualnie. Jednak w tej metodzie mamy również pełną elastyczność w sposobie manipulowania danymi, co jest zaletą, jeśli chcemy wykonywać złożone operacje.

Jeśli od początku utworzymy właściwość, która przechowuje numer wersji naszego obiektu, możemy użyć tego numeru w przyszłości, aby wykonać określoną migrację z tej wersji do dowolnej innej. Ustawiamy właściwość jako tylko do odczytu, więc nie możemy jej nadpisać w edytorze właściwości, chociaż jest ona nadal dostępna z konsoli Python.

# old_module.py
class OldObject:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "Area")
        obj.addProperty("App::PropertyString", "Version")
        obj.setEditorMode("Version", 1)
        obj.Length = 15
        obj.Area = 300
        obj.Version = "0.18"
        obj.Proxy = self
        self.Type = "Custom"

    def execute(self, obj):
        pass

Następnie, gdy chcemy zmigrować obiekt, implementujemy metodę onDocumentRestored i testujemy tę wersję.

# old_module.py
import FreeCAD as App
import objects.new_module as new_module
_wrn = App.Console.PrintWarning

class OldObject:
    def onDocumentRestored(self, obj):
        if hasattr(obj, "Version") and obj.Version:
            if obj.Version == "0.18":
                _migrate_from_018(obj)
            elif obj.Version == "0.19":
                _migrate_from_019(obj)

def _migrate_from_018(obj):
    old = dict()
    old["Area"] = obj.Area
    old["Length"] = obj.Length
    obj.removeProperty("Area")
    obj.removeProperty("Length")
    obj.removeProperty("Version")

    new_module.NewObject(obj)

    obj.GeneralArea = 3 * old["Area"]
    obj.Length = old["Length"]
    obj.Version = "0.20"
    _wrn("New proxy class used; properties migrated\n")

def _migrate_from_019(obj):
    ...

Nie zapisujemy wartości Version, ponieważ podczas migracji ustawimy nowy numer Version. Jak pokazano w przykładzie, możemy zaimplementować różne funkcje dla każdej odpowiedniej wersji obiektu, który zamierzamy zmigrować. Pomijamy migrację właściwości dostawcy widoku, ale przebiega ona według tego samego schematu.

Zalety i wady

Zalety

Wady

Uzupełnienie B2. Używanie wewnętrznych atrybutów klasy zamiast właściwości

Zamiast używać właściwości obiektu do przechowywania informacji o wersji, możemy użyć atrybutu klasy. W ten sposób "ukrywamy" informacje o wersji, ponieważ właściwości są zwykle publiczne i widoczne w edytorze właściwości, podczas gdy atrybutami klasy można manipulować tylko z konsoli Python. Atrybuty klas mogą być zapisywane i przywracane, jak wyjaśniono w Obiektchy generowanych skryptami.

# old_module.py
class OldObject:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "Area")
        obj.Length = 15
        obj.Area = 300
        obj.Proxy = self

        self.Type = "Custom"
        self.ver = "0.18"

    def execute(self, obj):
        pass

Atrybut ten jest kontrolowany poprzez przeglądanie atrybutu Proxy.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.Proxy.ver)
0.18

Następnie plik jest modyfikowany w celu migracji obiektu.

# old_module.py
import FreeCAD as App
import objects.new_module as new_module
_wrn = App.Console.PrintWarning

class OldObject:
    def onDocumentRestored(self, obj):
        if hasattr(obj.Proxy, "ver") and obj.Proxy.ver:
            if obj.Proxy.ver == "0.18":
                _migrate_from_018(obj)

def _migrate_from_018(obj):
    old = dict()
    old["Area"] = obj.Area
    old["Length"] = obj.Length
    obj.removeProperty("Area")
    obj.removeProperty("Length")

    new_module.NewObject(obj)

    obj.GeneralArea = 3 * old["Area"]
    obj.Length = old["Length"]
    _wrn("New proxy class used; properties migrated\n")

Kiedy zainstalujemy nową klasę, ta nowa klasa powinna ustawić nową wartość atrybutu version, na przykład self.ver = "0.20".

Uzupełnienie C. Metoda 3 bez usuwania starych właściwości o tej samej nazwie

Podobnie jak w Uzupełnieniu A, możemy napisać nową klasę, aby tworzyła właściwości tylko wtedy, gdy jeszcze ich nie ma. Korzystając z metody 3, zapisujemy wartości starszych właściwości, a następnie usuwamy starsze właściwości. Jeśli jednak nowe właściwości nazywają się tak samo jak starsze, nie musimy usuwać starszych, możemy po prostu ponownie użyć tej samej właściwości, ponieważ wiemy, że właściwość nie zostanie zduplikowana. Jeśli korzystamy z Uzupełnienia B, mamy również sposób na zapytanie o wersję.

# old_module.py
import FreeCAD as App
import objects.new_module as new_module
_wrn = App.Console.PrintWarning

class OldObject:
    def onDocumentRestored(self, obj):
        if hasattr(obj, "Version") and obj.Version:
            if obj.Version == "0.18":
                _migrate_from_018(obj)

def _migrate_from_018(obj):
    old = dict()
    old["Area"] = obj.Area
    obj.removeProperty("Area")

    new_module.NewObject(obj)

    obj.GeneralArea = 3 * old["Area"]
    obj.Version = "0.20"
    _wrn("New proxy class used; properties migrated\n")

Jak widzimy w przykładzie, stara właściwość Area jest usuwana i migrowana do nowej właściwości GeneralArea jak zwykle. Nie musimy usuwać Length ani Version, ponieważ w nowej klasie są one nadal używane z tą samą nazwą i nie zostaną ponownie utworzone ( uzupełnienie A). Ponieważ nie chcemy modyfikować Length, ta właściwość nie jest w ogóle dotykana; jest migrowana do nowej klasy po cichu. Aktualizujemy jednak Version do nowej wartości. Pomijamy migrację właściwości dostawcy widoku, ale przebiega ona według tego samego schematu.

Powinno to działać jak metoda 3, co oznacza, że stare właściwości są usuwane i tylko nowe właściwości pozostają w nowym obiekcie. Jedyną różnicą jest to, że pomijamy usuwanie i ponowne tworzenie właściwości, które nazywają się tak samo. Proces ten powinien działać tak długo, jak długo stara właściwość i nowa właściwość mają ten sam typ (na przykład App::PropertyLength lub App::PropertyArea), więc stara właściwość może przekazać swoją wartość bezpośrednio. Jeśli jednak nowa właściwość ma inny typ niż stara właściwość, wówczas stara właściwość powinna zostać usunięta, w przeciwnym razie stara właściwość całkowicie nadpisze nową właściwość, co prawdopodobnie nie jest tym, czego chcemy, ponieważ nowa klasa będzie oczekiwać nowego typu, a nie starego.

Zalety i wady

Zalety

Wady

Podsumowanie

Każda z metod ma zalecane zastosowanie:

Najlepiej unikać następujących metod:

Odnośniki internetowe